Medium 清新閱讀版
:連結
今天已經是第鐵人賽第24天了!
在前面的23天,與大家分享了許多撰寫 PHPUnit 測試程式碼所需的知識,之後的文章就讓我們來來模擬一些情境題,並在這些情境題底下,實際去設計測試案例函數吧!
作為第一個情境題,我們就選「網站文章」來當作第一個挑戰吧!
這邊我們假設網站是採前後端分離的設計,因此我們就專注在測試 API 的部分。
依據以上的使用案例,我們可規畫出以下 API:
GET /api/articles
GET /api/articles/{id}
GET /api/articles/{id}/comments
POST /api/articles/comments
接著就來實作 API 吧!
app/Http/Controllers/Api/ApiController.php
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use function response;
class ApiController extends Controller
{
public function respondJson($data)
{
return response()->json([
'data' => $data,
]);
}
public function respondNotFound()
{
return response()->json('', 404);
}
}
app/Http/Controllers/Api/ArticleController.php
<?php
namespace App\Http\Controllers\Api;
use App\Models\Article;
use App\Models\Comment;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class ArticleController extends ApiController
{
public function index(Request $request)
{
$articles = Article::all();
return $this->respondJson($articles);
}
public function show($id)
{
$article = Article::find($id);
if (empty($article)) {
return $this->respondNotFound();
}
return $this->respondJson($article);
}
public function comments(Request $request, $id)
{
$article = Article::find($id);
if (empty($article)) {
return $this->respondNotFound();
}
$comments = $article->comments;
return $this->respondJson($comments);
}
public function storeComment(Request $request, $id)
{
$user = Auth::user();
$article = Article::find($id);
if (empty($article)) {
return $this->respondNotFound();
}
$data = [
'article_id' => $article->id,
'content' => $request->input('comment'),
'user_id' => $user->id,
];
$comment = new Comment($data);
$comment->save();
return $this->respondJson($comment);
}
}
app/Models/Article.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
use HasFactory;
protected $fillable = [
'content',
];
public function comments()
{
return $this->hasMany(Comment::class);
}
}
app/Models/Comment.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
use HasFactory;
protected $fillable = [
'content',
'user_id',
'article_id',
];
public function article()
{
return $this->belongsTo(Article::class);
}
}
app/Models/User.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
}
routes/api.php
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\ArticleController;
Route::prefix('articles')->group(function() {
Route::get('', [ArticleController::class, 'index'])
->name('article.list');
Route::get('/{id}', [ArticleController::class, 'show'])
->where('id', '[0-9]+')
->name('article.one');
Route::get('/{id}/comments', [ArticleController::class, 'comments'])
->where('id', '[0-9]+')
->name('article.one.comments');
Route::post('/{id}/comments', [ArticleController::class, 'storeComment'])
->middleware('auth:api')
->where('id', '[0-9]+')
->name('article.one.comments.store');
});
database/migrations/2014_10_12_000000_create_users_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('users');
}
};
database/migrations/2022_10_02_174939_create_articles_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('articles', function (Blueprint $table) {
$table->id();
$table->text('content');
$table->integer('page_views');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('articles');
}
};
database/migrations/2022_10_08_172525_create_comments_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->integer('user_id');
$table->integer('article_id');
$table->text('content');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('comments');
}
};
這邊我們要準備的是各 Model 的 Factory 類別,以及批次產生測試資料的 Seeders:
User Factory
<?php
namespace Database\Factories;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Factories\Factory;
class UserFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array
*/
public function definition(): array
{
return [
'name' => $this->faker->name,
'email' => $this->faker->safeEmail,
'email_verified_at' => $this->faker->dateTime(),
'password' => bcrypt($this->faker->password),
'remember_token' => Str::random(10)
];
}
}
Article Factory
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Article>
*/
class ArticleFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition()
{
return [
'content' => $this->faker->text,
'page_views' => 0,
];
}
}
Comment Factory
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Comment>
*/
class CommentFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition()
{
return [
'content' => $this->faker->text,
];
}
}
User Seeder
<?php
namespace Database\Seeders;
use App\Models\User;
use App\Models\UserLog;
use Illuminate\Database\Seeder;
class UserSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
User::factory()
->count(10)
->create();
}
}
Article Seeder
<?php
namespace Database\Seeders;
use App\Models\Article;
use App\Models\Comment;
use App\Models\User;
use Illuminate\Database\Seeder;
class ArticleSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
$articles = Article::factory()
->count(10)
->create();
$users = User::all();
foreach ($articles as $article) {
$commentCount = random_int(1, 5);
for ($i = 0; $i < $commentCount; $i ++) {
$user = $users->random();
Comment::factory()
->create([
'user_id' => $user->id,
'article_id' => $article->id,
]);
}
}
}
}
到這邊為止,我們已經把測試目標準備好了,明天我們就來針對各使用案例來寫測試吧!